Imports BMD2Net

''' <summary>
''' Class for all classes to read BMD data
''' </summary>
Public Class clsDataBMD2
    Inherits clsData

    'local variable(s) to hold property value(s)
    Private _TimeNum As Integer
    Private _SegmentNum As Integer 'keeps track of current segment number; sequential reads loop through all times, then all stations

    Public Property BMD2 As clsBMD2

    Public Overrides Sub Initialize(ByVal _Filename As String)
        MyBase.Initialize(_Filename)
        mDataType = enumDataType.BMD2
        'MessageBox.Show("Logging in clsDataBMD2")
        'clsBMD2.LogLevelThreshold = clsBMD2.enumLogLevelThreshold.Debug
        _BMD2 = New clsBMD2(_Filename)
        With _BMD2
            For Each s As String In .SegmentNames
                If Not mStationIDs.Contains(s) Then mStationIDs.Add(s)
            Next

            'treat bmd file as a tabular data file, where the station ID (segment) and date-time are in columns 0 and 1, and each variable is in subsequent columns
            mFieldNames.Clear()
            mFieldNames.Add("Station_ID")
            mFieldNames.Add("Date_Time")
            ReDim mMinValue(.NumVariables + 2 - 1)
            ReDim mMaxValue(.NumVariables + 2 - 1)
            For v As Integer = 0 To .NumVariables - 1
                Dim PCode As String = .VariableName(v).Replace(Chr(0), "").Trim
                Dim num As Integer = 0
                While mPCodes.Contains(PCode)
                    num += 1
                    PCode &= num
                End While
                mPCodes.Add(PCode)
                mFieldNames.Add(PCode & IIf(.VariableUnits(v).Trim = "", "", " (" & .VariableUnits(v).Replace(Chr(0), "").Trim & ")"))
                .MinMaxForVariable(v, mMinValue(mFieldNames.Count - 1), mMaxValue(mFieldNames.Count - 1))
            Next
            mMinDate = _BMD2.NthMoment(0)
            mMaxDate = _BMD2.NthMoment(_BMD2.NumMoments - 1)
            _SegmentNum = 0
        End With
        IsFileOpen = True
    End Sub

    Public Overrides Property ActiveStationID() As String
        Get
            Return MyBase.ActiveStationID
        End Get
        Set(ByVal value As String)
            MyBase.ActiveStationID = value
            _SegmentNum = StationIDs.IndexOf(value)
        End Set
    End Property

    ''' <summary>
    ''' Read next line from file into memory, storing into mItems array
    ''' Also save mLastLineRead for error reporting
    ''' </summary>
    ''' <returns>True if successful, false if eof reached and no more data</returns>
    Public Overrides Function Read() As Boolean
        Try
            If Not MyBase.Read() Then Return False
            With _BMD2
                If _TimeNum > .NumMoments - 1 Then 'go to next segment
                    If mActiveStationIDSet Then
                        Return False 'only read one stationid
                    Else
                        _SegmentNum += 1
                        _TimeNum = 0
                        If _SegmentNum > .NumSegments - 1 Then Return False
                    End If
                End If
                mItems.Clear()
                mItems.Add(.SegmentNames(_SegmentNum))
                mItems.Add(.NthMoment(_TimeNum).ToString(MyDateTimeFormat))
                'note: BMD2 returns as [var,seg]
                Dim ConcAtTime(,) As Single = .DataAtTime(_TimeNum)
                For v As Integer = 0 To .NumVariables - 1
                    mItems.Add(ConcAtTime(v, _SegmentNum))
                Next
                _TimeNum += 1
                Return True
            End With
        Catch ex As Exception
            ErrorMsg(, ex)
            Return False
        End Try
    End Function

    Public Overrides Sub Rewind()
        MyBase.Rewind()
        _TimeNum = 0
        _SegmentNum = 0
    End Sub

    ''' <summary>
    ''' Determine percent of file that has been read
    ''' </summary>
    ''' <returns>Percent of file read</returns>
    Public Overrides Function PercentComplete() As Double
        Return mRecordNum * 100.0 / (_BMD2.NumMoments * _BMD2.NumSegments)
    End Function

    ''' <summary>
    ''' Determine min/max values for each pcodes regardless of station id; generic version uses brute force by looping over all entries
    ''' </summary>
    ''' <remarks></remarks>
    Protected Overrides Sub GetMinMaxDates()
        mMinDate = _BMD2.NthMoment(0)
        mMaxDate = _BMD2.NthMoment(_BMD2.NumMoments - 1)
    End Sub

    ''' <summary>
    ''' Return data table containing station IDs and other properties that will be used by frmFilter
    ''' For most data types will just return list of station IDs, but for BMD2 will override to return additional metadata
    ''' </summary>
    ''' <returns>Datatable containing unique station IDs</returns>
    ''' <remarks></remarks>
    Public Overrides Function GetStationTable() As DataTable
        'get unique metadata keys for all stations
        Dim lst As New List(Of String)
        For i As Integer = 0 To BMD2.NumSegments - 1
            For Each kv As KeyValuePair(Of String, String) In BMD2.SegmentMetaData(i)
                If Not lst.Contains(kv.Key) Then lst.Add(kv.Key)
            Next
        Next

        Dim dt As New DataTable
        dt.Columns.Add("Station ID")
        For Each s As String In lst
            dt.Columns.Add(s)
        Next
        For i As Integer = 0 To BMD2.NumSegments - 1
            Dim r As DataRow = dt.NewRow
            For Each kv As KeyValuePair(Of String, String) In BMD2.SegmentMetaData(i)
                r(kv.Key) = kv.Value
            Next
            dt.Rows.Add(r)
        Next
        Return dt
    End Function

    ''' <summary>
    ''' Get data table containing time series data for all stations and pcodes
    ''' Table will contain station_id, date-time, and all variable fields
    ''' </summary>
    Public Overloads Function GetDataTable() As System.Data.DataTable
        Dim dt As DataTable = MyBase.SetupDataTable(enumDataTableColumns.All)
        With _BMD2
            For s As Integer = 0 To .NumSegments - 1
                For v As Integer = 0 To .NumVariables - 1
                    For t As Integer = 0 To .NumMoments - 1
                        Dim r As DataRow = dt.NewRow
                        r(0) = .SegmentName(s)
                        r(1) = .NthMoment(t).ToString(MyDateTimeFormat)
                        'note: BMD2 returns as [var,seg]
                        For j As Integer = 0 To .NumVariables - 1
                            r.Item(j + 2) = .DataAtTime(t)(v, s)
                        Next
                        dt.Rows.Add(r)
                    Next
                Next
                RaiseProgress(s, .NumSegments - 1, "Loading data...")
            Next
        End With
        Return dt
    End Function

    ''' <summary>
    ''' Get data table containing time series data for a specified station id and pcode
    ''' Table will contain date-time field and result field
    ''' </summary>
    Public Overloads Function GetDataTable(ByVal StationID As String, ByVal PCode As String) As DataTable
        Dim staNum As Integer = -1
        If mStationIDs.Contains(StationID) Then
            staNum = mStationIDs.IndexOf(StationID)
        Else
            Throw New ApplicationException("BMD file does not contain the station: " & StationID)
            Return Nothing
        End If
        Dim itemNum As Integer = -1
        If ContainsFieldName(PCode) Then
            itemNum = mPCodes.IndexOf(PCode)
        Else
            Throw New ApplicationException("BMD file does not contain the column: " & PCode)
            Return Nothing
        End If
        Dim dt As DataTable = MyBase.GetDataTable(StationID, PCode)
        With _BMD2
            Dim conc() As Single = .DataOverTime(staNum, itemNum)
            For t As Integer = 0 To .NumMoments - 1
                Dim result As Single = conc(t)
                Debug.Assert(Not Single.IsNaN(result))
                dt.Rows.Add(.NthMoment(t), result)
                RaiseProgress(t, .NumMoments - 1, "Loading data...")
            Next
        End With
        Return dt
    End Function

    ''' <summary>
    ''' Get data table containing time series data for a all stations and pcode
    ''' Table will contain station field, date-time field and result field
    ''' </summary>
    Public Overloads Function GetDataTable(ByVal StationIDs() As String, ByVal PCode As String) As DataTable
        Dim StationNums(StationIDs.Length - 1) As Integer
        For i As Integer = 0 To StationIDs.Length - 1
            Dim staNum As Integer = -1
            If mStationIDs.Contains(StationIDs(i)) Then
                StationNums(i) = mStationIDs.IndexOf(StationIDs(i))
            Else
                Throw New ApplicationException("BMD file does not contain the station: " & StationIDs(i))
                Return Nothing
            End If
        Next
        Dim itemNum As Integer = -1
        If ContainsFieldName(PCode) Then
            itemNum = mPCodes.IndexOf(PCode)
        Else
            Throw New ApplicationException("BMD file does not contain the column: " & PCode)
            Return Nothing
        End If

        Dim dt As DataTable = MyBase.SetupDataTable(enumDataTableColumns.Station_Result)
        With _BMD2
            For i As Integer = 0 To StationNums.Length - 1
                Dim conc() As Single = .DataOverTime(StationNums(i), itemNum)
                For t As Integer = 0 To .NumMoments - 1
                    Dim t1 As Date = .NthMoment(t)
                    dt.Rows.Add(StationIDs(i), t1, conc(t))
                Next
                RaiseProgress(i, StationNums.Length - 1, "Loading data...")
            Next
        End With
        Return dt
    End Function

    ''' <summary>
    ''' Get data table containing all data in given time range for all station ids and pcode
    ''' Table will contain station and result field
    ''' </summary>
    ''' <param name="StationIDList">List of Desired Station IDs</param>
    ''' <param name="PCode">Desired PCode</param>
    ''' <param name="MinDate">Minimum date to include</param>
    ''' <param name="MaxDate">Maximum date to include</param>
    Public Overrides Function GetResults(StationIDList As Generic.List(Of String),
                                         LinkageType As enumLinkageType,
                                         PCode As String,
                                         MinDate As Date, MaxDate As Date,
                                         AggregateFunction As enumAggregateFunction) As Generic.Dictionary(Of String, Single)

        'if parseIJ then create dictionary linking station IDs with IJ values
        If (LinkageType = enumLinkageType.ParseIJK Or LinkageType = enumLinkageType.IJtoIJ) Then
            dictIJK_StationID = New Dictionary(Of String, String)
            For Each staID As String In mStationIDs
                Dim IJ As String = ""
                Select Case LinkageType
                    Case enumLinkageType.ParseIJK
                        TryParseIJK(staID, IJ)
                    Case enumLinkageType.IJtoIJ
                        With _BMD2
                            Dim idx As Integer = .SegmentIndex(staID)
                            If idx <> -1 Then
                                With .SegmentMetaData(idx)
                                    If .ContainsKey("I") And .ContainsKey("J") Then IJ = String.Format("{0},{1}", Val(.Item("I")), Val(.Item("J")))
                                    If .ContainsKey("K") Then IJ &= String.Format(",{0}", Val(.Item("K")))
                                End With
                            End If
                        End With
                End Select
                If IJ <> "" AndAlso Not dictIJK_StationID.ContainsKey(IJ) Then dictIJK_StationID.Add(IJ, staID)
            Next
        End If

        Dim itemNum As Integer = mPCodes.IndexOf(PCode)
        If itemNum = -1 Then Return Nothing

        If MaxDate < MinDate Then
            Throw New ApplicationException("BMD requested dates are invalid")
            Return Nothing
        End If

        Dim dictResults As New Generic.Dictionary(Of String, Single)

        With _BMD2

            Dim tmin As Integer = .MomentIndex(MinDate)
            Dim tmax As Integer = Math.Max(tmin, .MomentIndex(MaxDate) - 1)

            If MinDate <= mMinDate And MaxDate >= mMaxDate And AggregateFunction <= enumAggregateFunction.Max Then
                'special case: asking for min/max value over entire period of record--get from bmd file
                For Each StaID As String In StationIDList
                    Dim SegNum As Integer
                    Select Case LinkageType
                        Case enumLinkageType.SegmentNum
                            SegNum = Val(StaID) - 1
                        Case enumLinkageType.ParseIJK, enumLinkageType.IJtoIJ
                            Dim stationID As String = ""
                            If dictIJK_StationID.TryGetValue(StaID, stationID) Then
                                SegNum = .SegmentIndex(stationID)
                            Else
                                SegNum = -1
                            End If
                        Case enumLinkageType.StationID
                            SegNum = .SegmentIndex(StaID)
                    End Select
                    Dim result As Single = -1
                    If SegNum <> -1 AndAlso SegNum >= 0 AndAlso SegNum <= .NumSegments - 1 Then
                        Dim min, max As Single
                        .MinMaxForSegVar(SegNum, itemNum, min, max)
                        Select Case AggregateFunction
                            Case enumAggregateFunction.Max : result = max
                            Case enumAggregateFunction.Min : result = min
                            Case enumAggregateFunction.Num : result = .NumMoments
                        End Select
                    End If
                    dictResults.Add(StaID, result)
                Next
            ElseIf AggregateFunction = enumAggregateFunction.Num Then
                'just return number of time steps in period
                For Each StaID As String In StationIDList
                    dictResults.Add(StaID, tmax - tmin)
                Next
            Else
                ''must loop over date range and calculate values
                'For t As Integer = tmin To tmax
                '    'note: BMD2 returns as [var,seg]
                '    Dim Concs(,) As Single = .DataAtTime(t)
                '    Dim sum As Single = 0
                '    Dim min As Single = Single.MaxValue
                '    Dim max As Single = Single.MinValue

                '    For Each StaID As String In StationIDList
                '        If t = tmin Then 'initialize values
                '            Select Case AggregateFunction
                '                Case enumAggregateFunction.Min : dictResults.Add(StaID, Single.MaxValue)
                '                Case enumAggregateFunction.Max : dictResults.Add(StaID, Single.MinValue)
                '                Case enumAggregateFunction.Mean : dictResults.Add(StaID, 0)
                '            End Select
                '        End If
                '        Dim SegNum As Integer
                '        Select Case LinkageType
                '            Case enumLinkageType.SegmentNum
                '                SegNum = StaID - 1
                '            Case enumLinkageType.ParseIJK, enumLinkageType.IJtoIJ
                '                Dim stationID As String = ""
                '                If dictIJK_StationID.TryGetValue(StaID, stationID) Then
                '                    SegNum = .SegmentIndex(stationID)
                '                Else
                '                    SegNum = -1
                '                End If
                '            Case enumLinkageType.StationID
                '                SegNum = .SegmentIndex(StaID) 'note: segID is 1-based, but index is 0 based
                '        End Select
                '        If SegNum <> -1 AndAlso SegNum >= 0 AndAlso SegNum <= .NumSegments - 1 Then
                '            Dim conc As Single = Concs(itemNum, SegNum)
                '            Select Case AggregateFunction
                '                Case enumAggregateFunction.Min : dictResults(StaID) = Math.Min(dictResults(StaID), conc)
                '                Case enumAggregateFunction.Max : dictResults(StaID) = Math.Max(dictResults(StaID), conc)
                '                Case enumAggregateFunction.Mean : dictResults(StaID) += conc
                '            End Select
                '        End If
                '    Next
                'Next
                ''reset any missing values to -1
                'For Each StaID As String In StationIDList
                '    If dictResults.ContainsKey(StaID) Then
                '        If LinkageType = enumLinkageType.SegmentNum AndAlso StaID = -1 Then
                '            dictResults(StaID) = -1
                '        ElseIf (LinkageType = enumLinkageType.ParseIJK Or LinkageType = enumLinkageType.IJtoIJ) AndAlso Not dictIJK_StationID.ContainsKey(StaID) Then
                '            dictResults(StaID) = -1
                '        ElseIf LinkageType = enumLinkageType.StationID AndAlso .SegmentIndex(StaID) = -1 Then
                '            dictResults(StaID) = -1
                '        ElseIf AggregateFunction = enumAggregateFunction.Mean Then
                '            dictResults(StaID) /= (tmax - tmin + 1)
                '        End If
                '    End If
                'Next

                'must loop over date range and calculate values

                Dim dictResultList As New Dictionary(Of String, List(Of Double)) 'will contain time series of values

                For t As Integer = tmin To tmax
                    'note: BMD2 returns as [var,seg]
                    Dim Concs(,) As Single = .DataAtTime(t)

                    For Each StaID As String In StationIDList

                        If t = tmin Then 'initialize list
                            dictResultList.Add(StaID, New List(Of Double))
                        End If

                        Dim SegNum As Integer
                        Select Case LinkageType
                            Case enumLinkageType.SegmentNum
                                SegNum = StaID - 1
                            Case enumLinkageType.ParseIJK, enumLinkageType.IJtoIJ
                                Dim stationID As String = ""
                                If dictIJK_StationID.TryGetValue(StaID, stationID) Then SegNum = .SegmentIndex(stationID) Else SegNum = -1
                            Case enumLinkageType.StationID
                                SegNum = .SegmentIndex(StaID) 'note: segID is 1-based, but index is 0 based
                        End Select

                        If SegNum <> -1 AndAlso SegNum >= 0 AndAlso SegNum <= .NumSegments - 1 Then
                            Dim conc As Single = Concs(itemNum, SegNum)
                            dictResultList(StaID).Add(conc)
                        End If
                    Next
                Next
                'reset any missing values to -1
                For Each StaID As String In dictResultList.Keys
                    If LinkageType = enumLinkageType.SegmentNum AndAlso StaID = -1 Then
                        dictResults(StaID) = -1
                    ElseIf LinkageType = enumLinkageType.ParseIJK AndAlso Not dictIJK_StationID.ContainsKey(StaID) Then
                        dictResults(StaID) = -1
                    ElseIf LinkageType = enumLinkageType.StationID AndAlso .SegmentIndex(StaID) = -1 Then
                        dictResults(StaID) = -1
                    Else
                        Dim agg As Double = -1
                        Dim sum As Double = 0.0, min As Double = Double.MaxValue, max As Double = Double.MinValue
                        Dim sumlog As Double = 0.0, numlog As Integer = 0
                        Dim arPct(18) As Double

                        Select Case AggregateFunction
                            Case enumAggregateFunction.Min, enumAggregateFunction.Max, enumAggregateFunction.Mean
                                For Each d As Double In dictResultList(StaID)
                                    sum += d
                                    min = Math.Min(min, d)
                                    max = Math.Max(max, d)
                                Next
                            Case enumAggregateFunction.Geomean
                                For Each d As Double In dictResultList(StaID)
                                    If d > 0.0 Then
                                        numlog += 1
                                        sumlog += Math.Log(d)
                                    End If
                                Next
                            Case enumAggregateFunction.Num 'will calc below
                            Case Else
                                modProbability.CalcStats(dictResultList(StaID).ToArray, arPct)
                        End Select
                        Select Case AggregateFunction
                            Case enumAggregateFunction.Num : agg = dictResultList(StaID).Count
                            Case enumAggregateFunction.Min : agg = min
                            Case enumAggregateFunction.Max : agg = max
                            Case enumAggregateFunction.Mean : If dictResultList(StaID).Count > 0 Then agg = sum / dictResultList(StaID).Count
                            Case enumAggregateFunction.Geomean : If numlog > 0 Then agg = Math.Exp(sumlog / numlog)
                            Case enumAggregateFunction.Median : agg = arPct(9)
                            Case enumAggregateFunction.Pct5 : agg = arPct(0)
                            Case enumAggregateFunction.Pct10 : agg = arPct(1)
                            Case enumAggregateFunction.Pct25 : agg = arPct(4)
                            Case enumAggregateFunction.Pct75 : agg = arPct(14)
                            Case enumAggregateFunction.Pct90 : agg = arPct(17)
                            Case enumAggregateFunction.Pct95 : agg = arPct(18)
                        End Select
                        dictResults(StaID) = agg
                    End If
                Next
            End If
        End With
        Return dictResults
    End Function

    ''' <summary>
    ''' Get aggregate value for all data in given time range for station id and pcode
    ''' If requested station ID or PCode is not found, will throw exception and return double.minvalue
    ''' This routine is called by frmGIS and is used for animation: all values at a given time or range are assembled for rapid display
    ''' Has been overridden for BMD, and can also override for other types to make them faster (TO-DO item)
    ''' This version will return magnitude and direction tuple for display of vector data
    ''' </summary>
    ''' <param name="StationIDList">List of desired Station ID</param>
    ''' <param name="PCodeX">Desired PCode for x component</param>
    ''' <param name="PCodeY">Desired PCode for y component</param>
    ''' <param name="MinDate">Minimum date to include (inclusive)</param>
    ''' <param name="MaxDate">Maximum date to include (non-inclusive)</param>
    ''' <param name="AggregateFunction">How the raw data must be aggregated (note: only some functions are supported; caller will really only need means for velocity vectors)</param>
    Public Overrides Function GetResults(StationIDList As Generic.List(Of String),
                                         PCodeX As String, PCodeY As String,
                                         MinDate As Date, MaxDate As Date,
                                         AggregateFunction As enumAggregateFunction) As Generic.Dictionary(Of String, Tuple(Of Single, Single))

        Dim itemNumX As Integer = mPCodes.IndexOf(PCodeX)
        Dim itemNumY As Integer = mPCodes.IndexOf(PCodeY)

        If itemNumX = -1 Or itemNumY = -1 Then
            Throw New ApplicationException("PCodes were not recognized: " & PCodeX & ", " & PCodeY)
            Return Nothing
        End If

        If MaxDate < MinDate Then
            Throw New ApplicationException("BMD requested dates are invalid")
            Return Nothing
        End If

        Dim dictResults As New Generic.Dictionary(Of String, Tuple(Of Single, Single))

        With _BMD2

            'if all items in the station id list are numeric then assume is list of segment numbers rather than station IDs
            Dim IsSegNum As Boolean = True
            For Each s As String In StationIDList
                If Not IsNumeric(s) Then
                    IsSegNum = False
                    Exit For
                End If
            Next

            Dim tmin As Integer = .MomentIndex(MinDate)
            Dim tmax As Integer = Math.Max(tmin, .MomentIndex(MaxDate) - 1)

            If MinDate <= mMinDate And MaxDate >= mMaxDate And AggregateFunction <> enumAggregateFunction.Mean Then
                'special case: asking for min/max value over entire period of record--get from bmd file
                For Each StaID As String In StationIDList
                    Dim SegNum As Integer
                    If IsSegNum Then
                        SegNum = Val(StaID) - 1
                    Else
                        SegNum = .SegmentIndex(StaID)
                    End If
                    Dim l As Single = -1, a As Single = -1
                    If SegNum <> -1 AndAlso SegNum >= 0 AndAlso SegNum <= .NumSegments - 1 Then
                        Dim xmin, xmax, ymin, ymax As Single
                        .MinMaxForSegVar(SegNum, itemNumX, xmin, xmax)
                        .MinMaxForSegVar(SegNum, itemNumY, ymin, ymax)
                        Select Case AggregateFunction
                            Case enumAggregateFunction.Max
                                l = Math.Sqrt(xmax ^ 2 + ymax ^ 2)
                                a = Math.Atan2(ymax, xmax)
                            Case enumAggregateFunction.Min
                                l = Math.Sqrt(xmin ^ 2 + ymin ^ 2)
                                a = Math.Atan2(ymin, xmin)
                            Case enumAggregateFunction.Num
                                l = .NumMoments
                                a = 0
                        End Select
                    End If
                    dictResults.Add(StaID, New Tuple(Of Single, Single)(l, a))
                Next
            ElseIf AggregateFunction = enumAggregateFunction.Num Then
                'just return number of time steps in period
                For Each StaID As String In StationIDList
                    dictResults.Add(StaID, New Tuple(Of Single, Single)(tmax - tmin, 0))
                Next
            Else
                'must loop over date range and calculate values

                For Each StaID As String In StationIDList
                    If Not dictResults.ContainsKey(StaID) Then 'initialize values
                        Select Case AggregateFunction
                            Case enumAggregateFunction.Min : dictResults.Add(StaID, New Tuple(Of Single, Single)(Single.MaxValue, 0)) 'items are min length & associated angle
                            Case enumAggregateFunction.Max : dictResults.Add(StaID, New Tuple(Of Single, Single)(Single.MinValue, 0)) 'items are max length & associated angle
                            Case enumAggregateFunction.Mean : dictResults.Add(StaID, New Tuple(Of Single, Single)(0, 0)) 'items are sum xlength & sum ylength
                        End Select
                    End If
                    Dim SegNum As Integer
                    If IsSegNum Then SegNum = StaID - 1 Else SegNum = .SegmentIndex(StaID) 'note: segID is 1-based, but index is 0 based
                    If SegNum <> -1 AndAlso SegNum >= 0 AndAlso SegNum <= .NumSegments - 1 Then
                        Dim concX() As Single = .DataOverTime(SegNum, itemNumX)
                        Dim concY() As Single = .DataOverTime(SegNum, itemNumY)
                        For tt As Integer = tmin To tmax
                            Dim x As Single = concX(tt)
                            Dim y As Single = concY(tt)
                            Dim l As Single = Math.Sqrt(x * x + y * y)
                            Dim a As Single = Math.Atan2(y, x)
                            Select Case AggregateFunction
                                Case enumAggregateFunction.Min
                                    If l < dictResults(StaID).Item1 Then dictResults(StaID) = New Tuple(Of Single, Single)(l, a)
                                Case enumAggregateFunction.Max
                                    If l > dictResults(StaID).Item1 Then dictResults(StaID) = New Tuple(Of Single, Single)(l, a)
                                Case enumAggregateFunction.Mean
                                    dictResults(StaID) = New Tuple(Of Single, Single)(dictResults(StaID).Item1 + x, dictResults(StaID).Item2 + y)
                            End Select
                        Next

                    End If
                Next

                'reset any missing values to -1 and calculate mean
                For Each StaID As String In StationIDList
                    If dictResults.ContainsKey(StaID) Then
                        If IsSegNum And StaID = -1 Then
                            dictResults(StaID) = New Tuple(Of Single, Single)(-1, -1)
                        ElseIf Not IsSegNum AndAlso .SegmentIndex(StaID) = -1 Then
                            dictResults(StaID) = New Tuple(Of Single, Single)(-1, -1)
                        ElseIf AggregateFunction = enumAggregateFunction.Mean Then
                            Dim x As Single = dictResults(StaID).Item1
                            Dim y As Single = dictResults(StaID).Item2
                            Dim l As Single = Math.Sqrt(x * x + y * y) / (tmax - tmin + 1)
                            Dim a As Single = Math.Atan2(y, x)
                            dictResults(StaID) = New Tuple(Of Single, Single)(l, a)
                        End If
                    End If
                Next
            End If
        End With
        Return dictResults

    End Function

    ''' <summary>
    ''' If parsing I,J,K from metadata or station name, return list of unique K values (will be empty list if none found) (metadata overrides)
    ''' </summary>
    Public Overrides ReadOnly Property Layers As List(Of Integer)
        Get
            Dim lst As New List(Of Integer)
            For i As Integer = 0 To BMD2.NumSegments - 1
                Dim m As Dictionary(Of String, String) = BMD2.SegmentMetaData(i)
                Dim k As String = ""
                If m.ContainsKey("K") Then
                    k = Val(m("K"))
                Else
                    Dim ar() As String = StringSplit(BMD2.SegmentName(i), "K=", True)
                    If ar.Length = 2 Then k = Val(ar(1))
                End If
                If k <> "" AndAlso Not lst.Contains(k) Then lst.Add(k)
            Next
            lst.Sort()
            Return lst
        End Get
    End Property

    ''' <summary>
    ''' Retrieve all dates in BMD2
    ''' </summary>
    Public Overrides Function Dates() As List(Of DateTime)
        Dim lst As New List(Of DateTime)
        lst.AddRange(BMD2.moments)
        Return lst
    End Function

    Public Overrides Sub Close()
        MyBase.Close()
        _BMD2.Close()
        IsFileOpen = False
    End Sub

    Private Property IsFileOpen As Boolean = False

    Private _KeepFileOpen As Boolean = False
    ''' <summary>
    ''' If true, will not reopen then close the BMD file on major operations (like GetResults); 
    ''' the auto-close feature was added so BMD file is not locked for writing by WASP, but hurts animation performance
    ''' </summary>
    Public Property KeepFileOpen As Boolean
        Get
            Return _KeepFileOpen
        End Get
        Set(value As Boolean)
            _KeepFileOpen = value
            If Not _KeepFileOpen Then Close() 'force file to close when set to false
        End Set
    End Property

    ''' <summary>
    ''' Close then reopen BMD2 file
    ''' </summary>
    ''' <returns>True if successful</returns>
    Public Function Reopen() As Boolean
        If KeepFileOpen Then
            If IsFileOpen Then Exit Function
        Else
            If IsFileOpen Then Throw New IO.IOException("Tried to reopen BMD file that had not been closed") : Exit Function
        End If
        If _BMD2.Close() AndAlso _BMD2.Open(Me.FileName) Then
            IsFileOpen = True
            Return True
        Else
            IsFileOpen = False
            Return False
        End If
    End Function

End Class

